/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

/**
 * This class defines an exception ({@link fr.x9c.cadmium.kernel.Fail.Exception})
 * that may occur during Caml executeion. It also provides methods for raising
 * this exception.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.2
 * @since 1.0
 */
public final class Fail {

    /**
     * Index of 'Out_of_memory' exception. <br/>
     * One should notice that java memory will be exhausted first and thus,
     * this error will probably never be diagnosed by the code runner.
     */
    private static final int OUT_OF_MEMORY_EXCEPTION = 0;

    /** Index of 'Sys_error' exception. */
    private static final int SYS_ERROR_EXCEPTION = 1;

    /** Index of 'Failure' exception. */
    private static final int FAILURE_EXCEPTION = 2;

    /** Index of 'Invalid_argument' exception. */
    private static final int INVALID_EXCEPTION = 3;

    /** Index of 'End_of_file' exception. */
    private static final int END_OF_FILE_EXCEPTION = 4;

    /** Index of 'Division_by_zero' exception. */
    private static final int ZERO_DIVIDE_EXCEPTION = 5;

    /** Index of 'Not_found' exception. */
    private static final int NOT_FOUND_EXCEPTION = 6;

    /** Index of 'Match_failure' exception. */
    private static final int MATCH_FAILURE_EXCEPTION = 7;

    /** Index of 'Stack_overflow' exception. */
    private static final int STACK_OVERFLOW_EXCEPTION = 8;

    /** Index of 'Sys_blocked_io' exception. */
    private static final int SYS_BLOCKED_IO_EXCEPTION = 9;

    /** Index of 'Assert_failure' exception. */
    private static final int ASSERT_FAILURE_EXCEPTION = 10;

    /** Index of 'Undefined_recursive_module' exception. */
    private static final int UNDEFINED_RECURSIVE_MODULE_EXCEPTION = 11;

    /**
     * This class represents the different kinds of exceptions that may occur
     * in a caml program.
     */
    private static enum Kind {

        /** Exception is backed up by a given value. */
        VALUE(-1, null),

            /** Failure. */
            FAILURE(Fail.FAILURE_EXCEPTION, "Failure"),

            /** Invalid argument. */
            INVALID(Fail.INVALID_EXCEPTION, "Invalid_argument"),

            /** Out of memory. */
            OUT_OF_MEMORY(Fail.OUT_OF_MEMORY_EXCEPTION, "Out_of_memory"),

            /** Stack overflow. */
            STACK_OVERFLOW(Fail.STACK_OVERFLOW_EXCEPTION, "Stack_overflow"),

            /** System error. */
            SYS_ERROR(Fail.SYS_ERROR_EXCEPTION, "Sys_error"),

            /** End of file. */
            END_OF_FILE(Fail.END_OF_FILE_EXCEPTION, "End_of_file"),

            /** Division by zero. */
            ZERO_DIVIDE(Fail.ZERO_DIVIDE_EXCEPTION, "Division_by_zero"),

            /** Not found exception.*/
            NOT_FOUND(Fail.NOT_FOUND_EXCEPTION, "Not_found"),

            /** System blocked i/o. */
            SYS_BLOCKED_IO(Fail.SYS_BLOCKED_IO_EXCEPTION, "Sys_blocked_io");

        /** Index of message in global data. */
        private final int index;

        /** Predefined exception name. */
        private final String predefName;

        /**
         * Constructs an instance from an index.
         * @param idx index of message in global data
         * @param predefName exception predefined name
         */
        private Kind(final int idx, final String predefName) {
            this.index = idx;
            this.predefName = "caml_exn_" + predefName;
        } // end constructor(int, String)

        /**
         * Returns the index of the message in the global data.
         * @return the index of the message in the global data
         */
        private int getIndex() {
            return this.index;
        } // end method 'getIndex()'

        /**
         * Returns the predefined exception name.
         * @return the predefined exception name
         */
        private String getPredefName() {
            return this.predefName;
        } // end method 'getPredefName()'

    } // end enum 'Kind'

    /**
     * No instance of this class.
     */
    private Fail() {
    } // end empty constructor

    /**
     * Raises an exception.
     * @param v exception to raise - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void raise(final Value v) throws Fail.Exception {
        assert v != null : "null v";
        throw new Fail.Exception(Fail.Kind.VALUE, v, null);
    } // end method 'raise(Value)'

    /**
     * Raises an exception with constant.
     * @param tag exception constant - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void raiseWithConstant(final Value tag)
        throws Fail.Exception {
        assert tag != null : "null tag";
        raise(Value.createFromBlock(Block.createBlock(0, tag)));
    } // end method 'raiseWithConstant(Value)'

    /**
     * Raises an exception with tag and argument.
     * @param tag exception tag - should not be <tt>null</tt>
     * @param arg exception argument - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void raiseWithArg(final Value tag, final Value arg)
        throws Fail.Exception {
        assert tag != null : "null tag";
        assert arg != null : "null arg";
        raise(Value.createFromBlock(Block.createBlock(0, tag, arg)));
    } // end method 'raiseWithArg(Value, Value)'

    /**
     * Raises an exception with tag and arguments.
     * @param tag exception tag - should not be <tt>null</tt>
     * @param args exception argument - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void raiseWithArgs(final Value tag, final Value... args)
        throws Fail.Exception {
        assert tag != null : "null tag";
        assert args != null : "null args";
        final int len = args.length;
        final Block b = Block.createBlock(1 + len, 0);
        b.set(0, tag);
        for (int i = 0; i < len; i++) {
            b.set(1 + i, args[i]);
        } // end for
        raise(Value.createFromBlock(b));
    } // end method 'raiseWithArgs(Value, Value...)'

    /**
     * Raises an exception with tag and message.
     * @param tag exception tag - should not be <tt>null</tt>
     * @param msg exception message - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void raiseWithString(final Value tag, final String msg)
        throws Fail.Exception {
        assert tag != null : "null tag";
        assert msg != null : "null msg";
        raiseWithArg(tag, Value.createFromBlock(Block.createString(msg)));
    } // end method 'raiseWithString(Value, String)'

    /**
     * Raises an exception whose message is in global data.
     * @param k kind of exception (gives index of message in global data)
     *          - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    private static void raiseConstantFromField(final Kind k)
        throws Fail.Exception {
        assert k != null : "null k";
        assert k != Fail.Kind.VALUE : "k should not equal Kind.VALUE";
        throw new Fail.Exception(k, null, null);
    } // end method 'raiseConstantFromField(Kind)'

    /**
     * Raises an exception whose base message is in global data.
     * @param k kind of exception (gives index of message in global data)
     *          - should not be <tt>null</tt>
     * @param msg additional exception message
     * @throws Fail.Exception always
     */
    private static void raiseWithStringFromField(final Fail.Kind k,
                                                 final String msg)
        throws Fail.Exception {
        assert k != null : "null k";
        assert k != Fail.Kind.VALUE : "k should not equal Kind.VALUE";
        throw new Fail.Exception(k, null, msg);
    } // end method 'raiseWithStringFromField(Kind, String)'

    /**
     * Raises a 'Failure' exception.
     * @param msg exception message - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void failWith(final String msg) throws Fail.Exception {
        assert msg != null : "null msg";
        raiseWithStringFromField(Fail.Kind.FAILURE, msg);
    } // end method 'failWith(String)'

    /**
     * Raises a 'Invalid_argument' exception.
     * @param msg exception message - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void invalidArgument(final String msg)
        throws Fail.Exception {
        assert msg != null : "null msg";
        raiseWithStringFromField(Fail.Kind.INVALID, msg);
    } // end method 'invalidArgument(String)'

    /**
     * Raises a 'Invalid_argument' exception,
     * with "index out of bounds" message.
     * @throws Fail.Exception always
     */
    public static void arrayBoundError() throws Fail.Exception {
        invalidArgument("index out of bounds");
    } // end method 'arrayBoundError()'

    /**
     * Raises a 'Out_of_memory' exception.
     * @throws Fail.Exception always
     */
    public static void raiseOutOfMemory() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.OUT_OF_MEMORY);
    } // end method 'raiseOutOfMemory()'

    /**
     * Raises a 'Stack_overflow' exception.
     * @throws Fail.Exception always
     */
    public static void raiseStackOverflow() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.STACK_OVERFLOW);
    } // end method 'raiseStackOverflow()'

    /**
     * Raises a 'Sys_error' exception.
     * @param msg message of exception
     * @throws Fail.Exception always
     */
    public static void raiseSysError(final String msg) throws Fail.Exception {
        raiseWithStringFromField(Fail.Kind.SYS_ERROR, msg);
    } // end method 'raiseSysError()'

    /**
     * Raises a 'End_of_file' exception.
     * @throws Fail.Exception always
     */
    public static void raiseEndOfFile() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.END_OF_FILE);
    } // end method 'raiseEndOfFile()'

    /**
     * Raises a 'Division_by_zero' exception.
     * @throws Fail.Exception always
     */
    public static void raiseZeroDivide() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.ZERO_DIVIDE);
    } // end method 'raiseZeroDivide()'

    /**
     * Raises a 'Not_found' exception.
     * @throws Fail.Exception always
     */
    public static void raiseNotFound() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.NOT_FOUND);
    } // end method 'raiseNotFound()'

    /**
     * Raises a 'Sys_blocked_io' exception.
     * @throws Fail.Exception always
     */
    public static void raiseSysBlockedIO() throws Fail.Exception {
        raiseConstantFromField(Fail.Kind.SYS_BLOCKED_IO);
    } // end method 'raiseSysBlockedIO()'

    /**
     * Constructs a 'zero divide' error.
     * @return a 'zero divide' error
     */
    static Fail.Exception createZeroDivide() {
        return new Fail.Exception(Fail.Kind.ZERO_DIVIDE, null, null);
    } // end method 'createZeroDivide()'

    /**
     * Constructs a stack overflow error.
     * @return a stack overflow error.
     */
    static Fail.Exception createStackOverflow() {
        return new Fail.Exception(Fail.Kind.STACK_OVERFLOW, null, null);
    } // end method 'createStackOverflow()'

    /**
     * This class represents an exception thrown when a failure occurs.
     */
    public static final class Exception extends java.lang.Exception {

        /** Serialization UID. */
        static final long serialVersionUID = -6381915587892007156L;

        /** Kind of failure. */
        private final Kind kind;

        /**
         * Value of failure, if kind is
         * {@link fr.x9c.cadmium.kernel.Fail.Kind#VALUE}.
         */
        private final Value value;

        /** Failure message. */
        private final String msg;

        /**
         * Constructs an instance.
         * @param k kind of exception - should not be <tt>null</tt>
         * @param v value of exception, if any - may be <tt>null</tt>
         * @param m message of exception, if any - may be <tt>null</tt>
         */
        private Exception(final Kind k, final Value v, final String m) {
            super(k.toString());
            this.kind = k;
            this.value = v;
            this.msg = m;
        } // end constructor(Kind, Value, String)

        /**
         * Returns the underlying kind.
         * @return the underlying kind
         */
        Kind getKind() {
            return this.kind;
        } // end method 'getKind()'

        /**
         * Returns the underlying value.
         * @return the underlying value
         */
        Value getValue() {
            return this.value;
        } // end method 'getValue()'

        /**
         * Converts the exception into a value (native version).
         * @param runner code runner
         * @return the exception as a value
         */
        public Value asValue(final AbstractNativeRunner runner) {
            runner.setBacktraceInfo(this);
            if (this.kind == Fail.Kind.VALUE) {
                return this.value;
            } else {
                final Value tag = runner.getGlobal(this.kind.getPredefName());
                final Block bucket;
                if (this.msg != null) {
                    bucket = Block.createBlock(0,
                                               tag,
                                               Value.createFromBlock(Block.createString(this.msg)));
                } else {
                    bucket = Block.createBlock(0, tag);
                } // end if/else
                return Value.createFromBlock(bucket);
            } // end if/else
        } // end method 'asValue(AbstractNativeRunner)'

        /**
         * Converts the exception into a value (bytecode version).
         * @param globalData global data
         * @return the exception as a value
         */
        public Value asValue(final Value globalData) {
            if (this.kind == Fail.Kind.VALUE) {
                return this.value;
            } else {
                final Value tag =
                    globalData.asBlock().get(this.kind.getIndex());
                final Block bucket;
                if (this.msg != null) {
                    bucket = Block.createBlock(0,
                                               tag,
                                               Value.createFromBlock(Block.createString(this.msg)));
                } else {
                    bucket = Block.createBlock(0, tag);
                } // end if/else
                return Value.createFromBlock(bucket);
            } // end if/else
        } // end method 'asValue(Value)'

    } // end inner-class 'Exception'

} // end method 'Fail'
